{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import os\n",
    "import shutil\n",
    "import requests\n",
    "from datetime import datetime\n",
    "\n",
    "# 配置信息\n",
    "EDGE_BOOKMARKS_PATH = os.path.expanduser(\n",
    "    r'~\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Bookmarks')\n",
    "BACKUP_PATH = os.path.expanduser(\n",
    "    r'~\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Bookmarks.bak')\n",
    "TARGET_FOLDER_NAME = '疑似失效（待复核）'\n",
    "REQUEST_TIMEOUT = 10  # 请求超时时间（秒）\n",
    "\n",
    "def backup_bookmarks():\n",
    "    \"\"\"备份书签文件\"\"\"\n",
    "    if os.path.exists(EDGE_BOOKMARKS_PATH):\n",
    "        shutil.copy2(EDGE_BOOKMARKS_PATH, BACKUP_PATH)\n",
    "        print(f'已创建备份文件：{BACKUP_PATH}')\n",
    "\n",
    "def load_bookmarks():\n",
    "    \"\"\"加载书签数据\"\"\"\n",
    "    with open(EDGE_BOOKMARKS_PATH, 'r', encoding='utf-8') as f:\n",
    "        return json.load(f)\n",
    "\n",
    "def save_bookmarks(data):\n",
    "    \"\"\"保存书签数据\"\"\"\n",
    "    with open(EDGE_BOOKMARKS_PATH, 'w', encoding='utf-8') as f:\n",
    "        json.dump(data, f, ensure_ascii=False, indent=4)\n",
    "\n",
    "def check_url(url):\n",
    "    \"\"\"检查URL是否失效\"\"\"\n",
    "    try:\n",
    "        response = requests.head(url, \n",
    "                              allow_redirects=True, \n",
    "                              timeout=REQUEST_TIMEOUT,\n",
    "                              headers={'User-Agent': 'Mozilla/5.0'})\n",
    "        return response.status_code < 400\n",
    "    except (requests.exceptions.RequestException, requests.exceptions.Timeout):\n",
    "        return False\n",
    "\n",
    "def find_or_create_target_folder(bookmarks_data):\n",
    "    \"\"\"查找或创建目标文件夹\"\"\"\n",
    "    for root_key in ['bookmark_bar', 'other']:\n",
    "        root = bookmarks_data['roots'][root_key]\n",
    "        for child in root['children']:\n",
    "            if child.get('name') == TARGET_FOLDER_NAME:\n",
    "                return child\n",
    "\n",
    "    # 如果不存在则创建新文件夹\n",
    "    new_folder = {\n",
    "        'children': [],\n",
    "        'date_added': datetime.now().isoformat(),\n",
    "        'date_modified': datetime.now().isoformat(),\n",
    "        'id': str(int(datetime.now().timestamp())),\n",
    "        'name': TARGET_FOLDER_NAME,\n",
    "        'type': 'folder'\n",
    "    }\n",
    "    bookmarks_data['roots']['bookmark_bar']['children'].insert(0, new_folder)\n",
    "    return new_folder\n",
    "\n",
    "def process_bookmarks():\n",
    "    # 备份书签文件\n",
    "    backup_bookmarks()\n",
    "    \n",
    "    # 加载书签数据\n",
    "    bookmarks_data = load_bookmarks()\n",
    "    \n",
    "    # 准备失效链接列表\n",
    "    invalid_urls = []\n",
    "    \n",
    "    # 递归遍历书签节点\n",
    "    def traverse(node, path=''):\n",
    "        if node['type'] == 'folder':\n",
    "            current_path = f\"{path}/{node['name']}\" if path else node['name']\n",
    "            for child in node.get('children', []):\n",
    "                traverse(child, current_path)\n",
    "        elif node['type'] == 'url':\n",
    "            url = node.get('url', '')\n",
    "            if not check_url(url):\n",
    "                invalid_urls.append({\n",
    "                    'node': node,\n",
    "                    'parent': node.get('parent_id'),\n",
    "                    'path': path\n",
    "                })\n",
    "\n",
    "    # 遍历所有根节点\n",
    "    for root in bookmarks_data['roots'].values():\n",
    "        if 'children' in root:\n",
    "            for child in root['children']:\n",
    "                traverse(child)\n",
    "\n",
    "    # 如果没有失效链接则直接返回\n",
    "    if not invalid_urls:\n",
    "        print(\"未找到失效链接\")\n",
    "        return\n",
    "\n",
    "    # 查找或创建目标文件夹\n",
    "    target_folder = find_or_create_target_folder(bookmarks_data)\n",
    "    \n",
    "    # 移动失效链接\n",
    "    for item in invalid_urls:\n",
    "        node = item['node']\n",
    "        \n",
    "        # 从原位置移除\n",
    "        for root in bookmarks_data['roots'].values():\n",
    "            if 'children' in root:\n",
    "                root['children'] = [c for c in root['children'] if c.get('id') != node.get('id')]\n",
    "                for child in root['children']:\n",
    "                    if child['type'] == 'folder' and 'children' in child:\n",
    "                        child['children'] = [c for c in child['children'] if c.get('id') != node.get('id')]\n",
    "\n",
    "        # 添加到目标文件夹\n",
    "        node['parent_id'] = target_folder['id']\n",
    "        target_folder['children'].append(node)\n",
    "\n",
    "    # 保存修改\n",
    "    save_bookmarks(bookmarks_data)\n",
    "    print(f\"已移动 {len(invalid_urls)} 个失效链接到 {TARGET_FOLDER_NAME}\")\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "发生错误：[Errno 2] No such file or directory: '~\\\\AppData\\\\Local\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Bookmarks'\n",
      "已自动恢复备份文件\n"
     ]
    },
    {
     "ename": "FileNotFoundError",
     "evalue": "[Errno 2] No such file or directory: '~\\\\AppData\\\\Local\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Bookmarks.bak'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[4], line 7\u001b[0m\n\u001b[1;32m      6\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m----> 7\u001b[0m     \u001b[43mprocess_bookmarks\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m      8\u001b[0m     \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m操作完成，请手动打开Edge浏览器检查结果\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
      "Cell \u001b[0;32mIn[3], line 67\u001b[0m, in \u001b[0;36mprocess_bookmarks\u001b[0;34m()\u001b[0m\n\u001b[1;32m     66\u001b[0m \u001b[38;5;66;03m# 加载书签数据\u001b[39;00m\n\u001b[0;32m---> 67\u001b[0m bookmarks_data \u001b[38;5;241m=\u001b[39m \u001b[43mload_bookmarks\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m     69\u001b[0m \u001b[38;5;66;03m# 准备失效链接列表\u001b[39;00m\n",
      "Cell \u001b[0;32mIn[3], line 23\u001b[0m, in \u001b[0;36mload_bookmarks\u001b[0;34m()\u001b[0m\n\u001b[1;32m     22\u001b[0m \u001b[38;5;124;03m\"\"\"加载书签数据\"\"\"\u001b[39;00m\n\u001b[0;32m---> 23\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mEDGE_BOOKMARKS_PATH\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mr\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mencoding\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mutf-8\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m     24\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m json\u001b[38;5;241m.\u001b[39mload(f)\n",
      "File \u001b[0;32m~/.local/lib/python3.10/site-packages/IPython/core/interactiveshell.py:324\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m    318\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m    319\u001b[0m         \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m    320\u001b[0m         \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m    321\u001b[0m         \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m    322\u001b[0m     )\n\u001b[0;32m--> 324\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
      "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '~\\\\AppData\\\\Local\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Bookmarks'",
      "\nDuring handling of the above exception, another exception occurred:\n",
      "\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[4], line 12\u001b[0m\n\u001b[1;32m     10\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m发生错误：\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mstr\u001b[39m(e)\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m     11\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m已自动恢复备份文件\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m---> 12\u001b[0m \u001b[43mshutil\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcopy2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mBACKUP_PATH\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mEDGE_BOOKMARKS_PATH\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/lib/python3.10/shutil.py:434\u001b[0m, in \u001b[0;36mcopy2\u001b[0;34m(src, dst, follow_symlinks)\u001b[0m\n\u001b[1;32m    432\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39misdir(dst):\n\u001b[1;32m    433\u001b[0m     dst \u001b[38;5;241m=\u001b[39m os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mjoin(dst, os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mbasename(src))\n\u001b[0;32m--> 434\u001b[0m \u001b[43mcopyfile\u001b[49m\u001b[43m(\u001b[49m\u001b[43msrc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdst\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfollow_symlinks\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfollow_symlinks\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m    435\u001b[0m copystat(src, dst, follow_symlinks\u001b[38;5;241m=\u001b[39mfollow_symlinks)\n\u001b[1;32m    436\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m dst\n",
      "File \u001b[0;32m/usr/lib/python3.10/shutil.py:254\u001b[0m, in \u001b[0;36mcopyfile\u001b[0;34m(src, dst, follow_symlinks)\u001b[0m\n\u001b[1;32m    252\u001b[0m     os\u001b[38;5;241m.\u001b[39msymlink(os\u001b[38;5;241m.\u001b[39mreadlink(src), dst)\n\u001b[1;32m    253\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 254\u001b[0m     \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43msrc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mrb\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m fsrc:\n\u001b[1;32m    255\u001b[0m         \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m    256\u001b[0m             \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(dst, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mwb\u001b[39m\u001b[38;5;124m'\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m fdst:\n\u001b[1;32m    257\u001b[0m                 \u001b[38;5;66;03m# macOS\u001b[39;00m\n",
      "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '~\\\\AppData\\\\Local\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Bookmarks.bak'"
     ]
    }
   ],
   "source": [
    "if __name__ == '__main__':\n",
    "    # 检查Edge是否在运行\n",
    "    if os.path.exists(EDGE_BOOKMARKS_PATH + '.lock'):\n",
    "        input(\"检测到Edge浏览器正在运行，请先关闭Edge后按回车继续...\")\n",
    "    \n",
    "    try:\n",
    "        process_bookmarks()\n",
    "        print(\"操作完成，请手动打开Edge浏览器检查结果\")\n",
    "    except Exception as e:\n",
    "        print(f\"发生错误：{str(e)}\")\n",
    "        print(\"已自动恢复备份文件\")\n",
    "        shutil.copy2(BACKUP_PATH, EDGE_BOOKMARKS_PATH)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
